공유메모리 동시성 — 락 선택과 프로세스 간 함정

NOTE

공유메모리는 “같은 메모리를 보게 해주는 것”까지만 OS가 해 준다. 그 안에서 누가 언제 읽고 쓸지는 막아 주지 않으므로 여러 프로세스가 동시에 건드리면 race condition이 생긴다. 이 노트는 문제가 왜 생기는지, 락 수단이 세마포어만이 아니라는 것, 그리고 프로세스 간 락에서만 생기는 함정(락을 쥔 채 프로세스 사망 등)을 정리한다.

상위 허브: 공유메모리 한눈에 보기


1. 왜 동시성 이슈가 생기나

“같은 주소”는 아니다. 가상 주소는 프로세스마다 다를 수 있다(A는 0x7f00…, B는 0x7e90…). 하지만 가리키는 물리 메모리가 같으므로 한쪽이 쓰는 동안 다른 쪽이 읽거나 쓰면 꼬인다.

프로세스 A 가상 주소 ─┐
                      ├──→ 같은 물리 메모리  (count = 5)
프로세스 B 가상 주소 ─┘

시간 →
A: count 읽음(5) ───────────────→ 6 으로 씀
B:          count 읽음(5) ──→ 6 으로 씀
                                  결과: 6   (둘 다 +1 했는데 7이 아님)

count++ 한 줄도 읽기 → 더하기 → 쓰기의 여러 CPU 명령이라 중간에 다른 프로세스가 끼어들 수 있다. 스레드 동시성 문제와 본질이 같다 (→ 스레드 동시성 문제 정리, IPC와 스레드 메모리 공유 비교).

읽기만 하는 쪽도 안전하지 않다

구조체 여러 필드를 쓰는 도중에 읽으면 “일부만 갱신된 상태”(torn read)를 볼 수 있다. 읽는 쪽도 같은 락으로 보호하거나, 버전 번호 등으로 일관성을 확인해야 한다.


2. 락은 세마포어로만 하는가? — 아니다

방법설명특징
System V 세마포어 (semget, semop)커널이 관리하는 세마포어 세트전통 방식. SEM_UNDO로 프로세스가 죽으면 커널이 값을 자동 복구
POSIX 세마포어 (sem_open, sem_wait/sem_post)이름 있는 것, 또는 공유메모리 안에 sem_t를 두는 것System V보다 단순
프로세스 공유 뮤텍스pthread_mutex_t를 공유메모리 안에 두고 PTHREAD_PROCESS_SHARED 지정락이 데이터 옆에 있어 편하고, 경합이 없으면 커널 호출 없이 빠름
파일 락 (flock, fcntl)파일(또는 락 전용 파일)에 잠금단순하지만 용도가 한정적
원자적 연산 (__atomic, C11 atomic)락 없이 카운터 증감, CAS가장 빠르나 단순한 값에만 가능 → 락 없는 카운터
선택 가이드
  값 하나(카운터·플래그)만 갱신        → 원자적 연산
  여러 필드를 묶어서 일관되게 갱신      → 프로세스 공유 뮤텍스 (또는 세마포어)
  프로세스 사망 시 락 자동 복구가 중요   → System V 세마포어 + SEM_UNDO, 또는 robust 뮤텍스
  동시에 N개까지 허용(자원 풀)          → 카운팅 세마포어

세마포어와 뮤텍스의 개념 차이, 그리고 spinlock·rwlock·RCU 등 락 구현 전반과 select/epoll(락이 아닌 I/O 대기)과의 구분은 세마포어와 동시성 제어 4~5절을 참고.


3. 프로세스 간 락에서만 생기는 함정 3가지

3.1 락을 쥔 채 프로세스가 죽는 경우

스레드는 같은 프로세스라 함께 죽지만, 프로세스는 따로 죽는다. 락을 쥔 프로세스가 죽으면 락이 영원히 안 풀려 다른 프로세스가 무한 대기한다.

대응설명
System V 세마포어 SEM_UNDO프로세스 종료 시 커널이 그 프로세스가 올린/내린 세마포어 값을 되돌린다
robust 뮤텍스 (PTHREAD_MUTEX_ROBUST)소유자가 죽은 뮤텍스를 다음에 잠그는 쪽이 EOWNERDEAD를 받는다 → 데이터를 복구한 뒤 pthread_mutex_consistent

3.2 일반 뮤텍스는 프로세스 간에 안 통한다

속성 없이 만든 pthread_mutex_t는 같은 프로세스의 스레드끼리만 보장된다. 공유메모리에 둔다고 되는 것이 아니고 PTHREAD_PROCESS_SHARED를 반드시 지정해야 한다.

struct Shared {
    pthread_mutex_t lock;     /* 락을 보호 대상 데이터와 같은 공유메모리 안에 둔다 */
    int             count;
};
 
/* 생성자만 1번 초기화 */
pthread_mutexattr_t a;
pthread_mutexattr_init(&a);
pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED);   /* 프로세스 간 공유 */
pthread_mutexattr_setrobust(&a, PTHREAD_MUTEX_ROBUST);      /* 소유자 사망 감지 */
pthread_mutex_init(&s->lock, &a);
 
/* 사용하는 모든 프로세스 */
int r = pthread_mutex_lock(&s->lock);
if (r == EOWNERDEAD) {                 /* 이전 소유자가 락을 쥔 채 죽음 */
    /* 공유 데이터가 중간 상태일 수 있으니 점검·복구 후 */
    pthread_mutex_consistent(&s->lock);
}
s->count++;
pthread_mutex_unlock(&s->lock);

3.3 락 객체 자체의 위치와 레이아웃

  • 락(뮤텍스, sem_t)을 보호 대상과 같은 구조체 안에 두는 패턴이 흔하다. 이때 포인터가 아닌 값으로 두고(포인터는 프로세스마다 주소가 다름), 구조체 레이아웃(패딩)이 모든 프로세스에서 같아야 한다 → 구조체 패딩과 멤버 정렬.
  • 초기화는 생성자가 한 번만. 사용 측이 pthread_mutex_init을 다시 부르면 이미 잠겨 있는 락을 덮어쓰는 사고가 난다.

4. 동시성 체크리스트

점검 항목확인
공유 데이터를 쓰는 모든 경로가 같은 락을 쓰는가한 군데라도 빠지면 의미 없음
읽는 쪽도 락(또는 일관성 확인)을 쓰는가torn read 방지
락이 프로세스 간 공유 속성인가PTHREAD_PROCESS_SHARED / 세마포어
락 보유 중 프로세스 사망을 고려했는가SEM_UNDO / robust 뮤텍스
락 초기화가 한 곳(생성자)에서 한 번만 이루어지는가재초기화 사고 방지
구조체 안에 포인터를 저장하지 않았는가오프셋 사용 (mmap 노트)
락 순서가 일정한가 (여러 락 사용 시)데드락 방지

5. 한눈에 정리

질문답
동시성 이슈가 나오나그렇다. 가상 주소는 달라도 같은 물리 메모리라서 race가 생긴다
락은 세마포어뿐인가아니다. 프로세스 공유 뮤텍스, 파일 락, 원자적 연산도 있다
일반 뮤텍스를 공유메모리에 두면안 된다. PTHREAD_PROCESS_SHARED 필수
락 보유 중 프로세스가 죽으면무한 대기 위험 → SEM_UNDO / robust 뮤텍스(EOWNERDEAD)
읽기만 해도 락이 필요한가여러 필드를 읽는다면 필요 (torn read)

관련 문서